CONSORT Diagrams
1 Overview
CONSORT diagrams depict “progress through the phases of a parallel randomized trial of two groups (that is, enrollment, intervention allocation, follow-up, and data analysis)” (http://www.consort-statement.org/consort-statement/flow-diagram), and are often required with the submission of a publication.
Word and pdf templates of a CONSORT diagram are available, but manually filling these out leaves a lot to be desired; it loses reproducibility, is error prone, and it becomes tedious when having to update an analysis.
One possible solution is to use Graphviz, a data visualization software along with the available integration into R and RStudio and glue for string interpolation.
2 Graphviz and R/RStudio
The DiagrammeR package brings integration of Graphviz and mermaid.js into R, allowing diagrams to easily be created and rendered. For more information, there’s a great blog post by RStudio here.
In short, RStudio has a built in editor for .dot and .mmd files that can then be rendered by DiagrammeR:
Figure 2.1: Graphviz code and output in RStudio
dot and mmd code can also be written directly in R code and the passed as a string to DiagrammeR::grViz or DiagrammeR::mermaid:
One major disadvantage here is you lose out on some of the convenience factors of a text editor that supports a language (e.g. syntax highlighting, auto-complete, and debugging tools).
3 Variable Interpolation
To get the full benefit of building a consort diagram programmatically, we need the ability to fill in values based on a variable derived in R. DiagrammeR has built in support for string substitution, but the syntax is little clunky and creates a dependency on DiagrammeR::grViz to render a diagram.
Another solution would be to use the string interpolation capabilities of the R package glue.
Note that in Figure 2.1, sample size values were wrapped in < >. This served two purposes:
Placeholders during development
Indicators for
glueto interpolate a value
Note, by default, glue uses curly brackets for the opening and closing delimiters. <> was chosen because they aren’t as common in the DOT language.
greeting <- "Hello"
name <- "John"
glue::glue("{greeting}, {name}. How are you?")
## Hello, John. How are you?With this setup, we can read in a .dot file and interpolate the values to something we calculated in R.
4 CONSORT Example
Let’s say we create a file named consort.dot in our working directory with the following contents:
digraph consort
{
graph [pad = 0.2, nodesep = .5, ranksep=.6]
node [fontname = Helvetica, shape = box, width = 6, margin = .2]
assessed [label = "Assessed for eligibility (n = <assessed>)", width = 4]
excluded [label = "Excluded (n = <excluded_total>) \l" +
"• Not meeting inclusion criteria (n = <excluded_crit>)\l" +
"• Declined to participate (n = <excluded_dec>)\l" +
"• Other reasons (n = <excluded_oth>)\l",
width = 4]
randomized [label = "Randomized (n = <randomized>)", width = 3]
treatment [label = "Allocated to intervention (n = <interv_total>)\l" +
"• Received allocated intervention (n = <interv_rec>)\l" +
"• Did not receive allocated intervention (give reasons) (n = <interv_no>)\l"]
control [label = "Allocated to intervention (n = <interv_total>)\l" +
"• Received allocated intervention (n = <interv_rec>)\l" +
"• Did not receive allocated intervention (give reasons) (n = <interv_no>)\l"]
lostC [label = "Lost to follow-up (give reasons) (n = <lost_fu>)\l\l" +
"Discontinued intervention (give reasons) (n = <disc>)\l"]
lostT [label = "Lost to follow-up (give reasons) (n = <lost_fu>)\l\l" +
"Discontinued intervention (give reasons) (n = <disc>)\l"]
analyzedC [label = "Analyzed (n = <analyzed>)\l" +
"• Excluded from analysis (give reasons) (n = 0)\l"]
analyzedT [label = "Analyzed (n = <analyzed>)\l" +
"• Excluded from analysis (give reasons) (n = 0)\l"]
blank [label = "", width = 0.01, height = 0.01]
{ rank = same; blank excluded }
assessed -> blank[dir = none];
randomized -> {treatment control};
blank -> excluded[minlen = 3];
blank -> randomized;
treatment -> lostT;
control -> lostC;
lostC -> analyzedC;
lostT -> analyzedT;
}Then, we derive some values to fill the diagram in with. These normally would already be derived in your analysis.
assessed <- 1000
excluded_total <- 100
excluded_crit <- 70
excluded_dec <- 20
excluded_oth <- 10
randomized <- 900
interv_total <- 450
interv_rec <- 400
interv_no <- 50
lost_fu <- 23
disc <- 2
analyzed <- interv_total - lost_fu - discFinally, we read in the file, interpolate the values with glue, then render with DiagrammeR.